package jp.dodododo.dao.handler.impl;

import static jp.dodododo.janerics.GenericsTypeUtil.*;

import java.lang.reflect.Type;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import jp.dodododo.dao.IterationCallback;
import jp.dodododo.dao.columns.ResultSetColumn;
import jp.dodododo.dao.exception.NoParameterizedException;
import jp.dodododo.dao.impl.EmptyIterationCallback;
import jp.dodododo.dao.object.ObjectDesc;
import jp.dodododo.dao.object.ObjectDescFactory;
import jp.dodododo.dao.object.PropertyDesc;
import jp.dodododo.dao.types.JavaType;
import jp.dodododo.dao.util.ClassUtil;
import jp.dodododo.dao.util.TypesUtil;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

/**
 * 
 * @author Satoshi Kimura
 */
public class BeanResultSetHandler<T> extends AbstractResultSetHandler<T> {
	private Class<T> beanClass;

	public BeanResultSetHandler(IterationCallback<T> callback, Class<T> beanClass) {
		super(callback);
		this.beanClass = beanClass;
	}

	private BeanResultSetHandler(IterationCallback<T> callback, Class<T> beanClass, Map<Class<?>, Map<String, Object>> rowCaches) {
		this(callback, beanClass);
		this.rowCaches = rowCaches;
	}

	@Override
	protected T createRow(ResultSet rs, List<ResultSetColumn> resultSetColumnList) throws SQLException {
		T row = newInstance();

		setValues(rs, resultSetColumnList, row);

		ObjectDesc objectDesc = ObjectDescFactory.getObjectDesc(beanClass);

		boolean isReturnNull = false;

		// For Relation
		for (int i = 0; i < objectDesc.getPropertyDescSize(); i++) {
			PropertyDesc propertyDesc = objectDesc.getPropertyDesc(i);
			boolean b = setRelationValues(rs, resultSetColumnList, propertyDesc, row);
			isReturnNull = b ? b : isReturnNull;
		}

		if (isReturnNull == true) {
			return null;
		}
		return row;
	}

	private boolean setRelationValues(ResultSet rs, List<ResultSetColumn> resultSetColumnList, PropertyDesc propertyDesc, T target)
			throws SQLException {
		boolean isReturnNull = false;
		Class<?> propertyType = propertyDesc.getPropertyType();
		if (propertyDesc.hasWriteMethod() == false) {
			return isReturnNull;
		}
		if (propertyDesc.isEnumType() == true) {
			return isReturnNull;
		}
		IterationCallback<?> emptyCallback = EmptyIterationCallback.getInstance();
		Class<?> javaType = TypesUtil.getJavaType(propertyType).getType();
		if (Object.class.equals(javaType)) {
			if (propertyDesc.isCollectionType() == true) { // OneToMany,ManyToMany
				Type elementType = getElementTypeOfCollection(propertyDesc.getGenericPropertyType());
				if (elementType == null) {
					throw new NoParameterizedException(target.getClass(), propertyDesc.getPropertyName());
				}
				Class<?> nestedBeanType = getRawClass(elementType);
				if (nestedBeanType == null) {
					throw new NoParameterizedException(target.getClass(), propertyDesc.getPropertyName());
				}
				@SuppressWarnings("unchecked")
				Object nestedBean = new BeanResultSetHandler(emptyCallback, nestedBeanType, this.rowCaches).createRow(rs,
						resultSetColumnList);
				String key = getKey(target);
				Map<String, Object> rowCache = getRowCache(target.getClass());
				if (rowCache.containsKey(key) == true) {
					@SuppressWarnings("unchecked")
					T tmp = (T) rowCache.get(key);
					target = tmp;
					isReturnNull = true;
				} else {
					rowCache.clear();
					rowCache.put(key, target);
				}
				Object value = propertyDesc.getValue(target);
				if (value == null) {
					value = new ArrayList<Object>();
					propertyDesc.setValue(target, value);
				}
				if (nestedBean != null) {
					@SuppressWarnings( { "unchecked", "unused" })
					boolean ignore = ((Collection) value).add(nestedBean);
				}
			} else { // Other
				@SuppressWarnings("unchecked")
				Object nestedBean = new BeanResultSetHandler(emptyCallback, propertyType, this.rowCaches)
						.createRow(rs, resultSetColumnList);
				propertyDesc.setValue(target, nestedBean);
			}
		}
		return isReturnNull;
	}

	private Map<String, Object> getRowCache(Class<?> clazz) {
		if (rowCaches.containsKey(clazz) == true) {
			return rowCaches.get(clazz);
		} else {
			Map<String, Object> ret = new HashMap<String, Object>();
			rowCaches.put(clazz, ret);
			return ret;
		}
	}

	private void setValues(ResultSet rs, List<ResultSetColumn> resultSetColumnList, T row) throws SQLException {
		ObjectDesc objectDesc = ObjectDescFactory.getObjectDesc(beanClass);

		for (Iterator<ResultSetColumn> iter = resultSetColumnList.iterator(); iter.hasNext();) {
			ResultSetColumn column = iter.next();
			String name = column.getName();
			if (objectDesc.hasPropertyDesc(name)) {
				PropertyDesc propertyDesc = objectDesc.getPropertyDesc(name);
				Class<?> propertyType = propertyDesc.getPropertyType();
				JavaType<?> javaType = TypesUtil.getJavaType(propertyType);
				Object value = javaType.getValue(rs, name);
				propertyDesc.setValue(row, value);
			}
		}
	}

	private Map<Class<?>, Map<String, Object>> rowCaches = new HashMap<Class<?>, Map<String, Object>>();

	protected T newInstance() {
		return ClassUtil.newInstance(beanClass);
	}

	private String getKey(Object row) {
		return ToStringBuilder.reflectionToString(row, KEY_STYLE);
	}

	private static final ToStringStyle KEY_STYLE = new ToStringStyleNoCollectionPropertyOnly();

	public static class ToStringStyleNoCollectionPropertyOnly extends ToStringStyle {
		private static final long serialVersionUID = 6709065090286888648L;

		public ToStringStyleNoCollectionPropertyOnly() {
			setUseShortClassName(true);
			setUseIdentityHashCode(false);
		}

		@Override
		public void append(StringBuffer buffer, String fieldName, Object value, Boolean fullDetail) {
			if (value == null) {
				return;
			}
			if (value instanceof Collection) {
				return;
			}
			super.append(buffer, fieldName, value, fullDetail);
		}
	}
}
